Share via


いろいろな場所へ

Zune で動くゲームを作成する

Mike Calligaro

目次

C といえばシャープ
ゲームの詳細
ピクシー ダストの上のスプライト
アセット名とカラー キーについて
動かしてみよう
ボックスの中のスプライト
音を鳴らす
高度な描画
後はあなたしだい

私の息子は 3 歳のときから Xbox で遊んでいますが、プログラミングに興味を示し始めています。それも特にゲーム作りにです。そこで、XNA Game Studio をダウンロードし、息子に教えてやれるように自分で試してみたわけです。自分でゲームを作ってみたいと常々思っていたこととはまったく関係ありません。うそではありません。

XNA Game Studio 3.0 では、Zune 用のゲームを作成する機能が追加されました。つまり、Zune はモバイル開発プラットフォームになったのです。そういったわけで、今回の「いろいろな場所へ」はゲームです。既に XNA を利用している開発者には、わかりきった内容でしょう。しかし、私のようにゲームの開発には夢中でもまったく未経験のプログラマは、どうぞこのまま読み進んでください。

C といえばシャープ

私はマイクロソフトに入社してから 15 年経ちますが、そのほとんどはシステムとドライバの開発に携わってきました。使用してきた言語は C++ で、それも必要最小限の部分だけです。MFC や Microsoft .NET Framework などのランタイムを使用したことはほとんどありません。最近まで、STL というスペルを書いたことはなく、ましてや使用したことなどありませんでした。

それが私の本業です。しかし、オフのときには C# で遊ぶのが好きです。たいていは Windows Mobile デバイス用のちょっとしたアプリケーションを書いていますし、PC 用のアプリケーションを作ることもあります。私は他の多くのネイティブ開発者ほど C++ からひどい仕打ちを受けてはいませんが、それでも C# でプログラミングすると少しめまいを感じます。ほんのわずかなコード行で多くのことを実行できます。C# はおもしろすぎるので規制品にされたに違いないと私は確信しています。

おもしろさの範疇で言うならば、XNA Game Studio は C# にステロイド剤を足したようなものです。あちらのチームは、ゲーム開発を簡単にする仕事でめざましい成果を上げています。フレームワークは単純で、難しい処理はほとんどが自動的に行われ、ゲーム開発のさまざまな側面を学ぶことができるサンプルが大量にリリースされています。

出発点は creators.xna.com です。そこから XNA Game Studio 3.0 を無料でダウンロードできます。Visual Studio 2008 のさまざまなバージョンのいずれかを既に使用している場合、XNA Game Studio はそれに統合されます。Visual Studio 2008 がない場合でも心配は無用です。XNA Game Studio は、無料の Visual C# Express Edition でも使用できます (つまり、説明には Visual Studio を使用しますが、Visual C# Express Edition を使用している場合はそれに置き換えてください)。

creators.xna.com の Web サイトにも、開発に役立つ情報が豊富にあります。ページの上部にある [EDUCATION] リンクをクリックして、初心者ガイド、サンプル、ハウツーなどを探してください。「初心者向け 2D ゲーム ガイド」は特に役に立つドキュメントで、XNA Game Studio と一緒にインストールされます。インストールされるドキュメントに Web にはない情報が含まれることがあります。Visual Studio では、[ヘルプ] メニューの [目次] をクリックしてフィルタを [XNA Game Studio 3.0] に設定することで、ドキュメントを表示できます。

XNA Game Studio では、1 つのコード ベースを作成して、それを Xbox、PC、および Zune に展開できます。ここで行うすべてのことは、3 種類のプラットフォームすべてで動作します。PC または Zune 用の開発に必要なものは無料のダウンロードだけですが、Xbox 用の開発には Premium メンバシップが必要であり年会費がかかります。

ゲームの詳細

XNA Game Studio 3.0 をインストールしたら、[ファイル] メニューの [新規作成] をポイントし、[プロジェクト] をクリックして、Zune 用の Hello World ゲームを作成できます。次に、[Visual C#]、[XNA Game Studio 3.0] の順に選択し、[Zune Game (3.0)] をクリックします。これによって生成されるプログラムでは実際には Hello World と表示されませんが、入力を監視し、背景を青で描画して、要求されると終了します。

何か本当に複雑なものを予想していたとしたら、うれしい驚きを感じることでしょう。ゲームのインフラストラクチャは全体でも、6 個の関数に分かれた約 20 行のコードです。これらの関数について次に説明します。

コンストラクタはごく標準的なもので、よくある C# オブジェクトのコンストラクタです。他のコンストラクタと同じように扱います。

Initialize は、コンストラクタが完了した後で呼び出されます。この関数を使用してさらに初期化を行います。特に、グラフィックス システムにかかわることなどです。

LoadContent は Initialize の後で呼び出され、イメージとサウンドを読み込みます。

UnloadContent はゲームの終了時に呼び出され、コンテンツをアンロードできるようにします。ただし、ほとんどのイメージとサウンドのアンロードは自動的に処理されるので、通常ここでは何も行う必要はありません。

Update はおそらく、ゲーム開発が普通のアプリケーション開発と最も異なる部分です。メッセージ ループの代わりに、ゲームは定期的に入力をポーリングし、入力に対して行う処理を計算し、計算の結果を描画します。そのほとんどは Update の中で発生するので、大部分のコードはこの関数に存在します。

Draw は各 Update の後で呼び出され、イメージを描画します。

これだけです。プログラムは非常に短いですが、開始した後に画面を青で塗りつぶし、入力を読み取り、戻るボタンが押されると終了します。簡単でしょう?ゲームの開発は難しいと思っていましたか。XNA Game Studio を使えばそんなことはありません。

ピクシー ダストの上のスプライト

簡単ではあったかもしれませんが、青い背景で終了するのでは、魅力的なゲームとはいえません。ゲームを作成するのなら、実際のイメージが必要です。さいわい、画面に画像を表示するのは難しくありません。

すぐにコードをお見せしますが、まず最初にイメージをプロジェクトに読み込む必要があります。Visual Studio には、ソリューション エクスプローラという名前のウィンドウがあります (表示されていない場合は、[表示] メニューで見つけてください)。ソリューション エクスプローラで [Content] を右クリックし、[追加]、[既存の項目] の順にクリックします。ここからは、任意の .bmp、.jpg、.png、.tga、または .dds イメージ ファイルを指定できます。

player.jpg という名前の付いた人物写真があり、それを [Content] に追加するものとします。図 1 に、このイメージを画面に表示するためのコードを示します。

図 1 イメージの表示

// Add these member variables
Texture2D texture;
Vector2 position;

// Add this line to the constructor
position = new Vector2(100, 100);

// Add this line to LoadContent()
texture = Content.Load<Texture2D>("player");

// And add these lines to Draw();
spriteBatch.Begin();
spriteBatch.Draw(texture, position, Color.White);
// (Draw the rest of your sprites)
spriteBatch.End();

これらのコードを説明する前に、用語についてお話しします。XNA Game Studio は、Xbox 用の開発システムとして始まりました。そのため、すべては 3D グラフィックスが基になっています。2D ゲーム (Zune には 3D レンダラがないので、2D ゲームにする必要があります) は実際には、すべてのものが薄い 3D ゲームです。そのため、コードのあちこちで 3D の用語が使用されています。たとえば、スプライトは 2D ゲームのイメージでは普通の用語ですが、テクスチャは 3D ゲームからの言葉です。XNA Game Studio に関する限り、スプライトは実際にはテクスチャがマップされた薄い 3D の四角形です。

Content.Load 関数はアセット名を受け取ります。これは、既定では拡張子のないファイル名です。player.jpg という名前のイメージを読み込んだので、Content.Load には "player" という文字列を渡します。LoadContent 関数には、SpriteBatch を割り当てる行が既にあることがわかります。すべての描画はこの SpriteBatch で行われます。Draw 関数では、すぐに処理を開始し、すべてのスプライトを描画し、終了することを SpriteBatch に指示します。

position メンバ変数は、スプライトを描画する x と y の位置を保持しているベクトル構造体です。この例では、ピクセル位置 (100, 100) を使用しました。Color はスプライトに追加できる色合いです。White を使用すると、イメージの色は変化しません。

つまり、イメージを画面に表示するために必要なことは、テクスチャを読み込み、読み込んだテクスチャを描画するように spriteBatch に指示することだけです。最初のイメージには基本的に 4 行のコードを使用し、以降の各イメージではさらに 2 行を使用します。

アセット名とカラー キーにつて

できれば、この時点で player イメージを画面に表示し、難しくないことを確認してください。しかし、完全には満足できないでしょう。コードでイメージのファイル名を使用しなくてもいいようにしたい場合は、どうしたらよいでしょうか。また、さらに重要なこととして、読み込んだ写真が四角形で、player が四角形ではないとしたらどうしますか。player だけを描画して背景を描画しないようにするにはどうすればよいでしょう。

これらの疑問に答えるには、読み込んだイメージのプロパティについて考える必要があります。ソリューション エクスプローラに戻り、ファイル (この例では player.jpg) を右クリックします。[プロパティ] をクリックします。ソリューション エクスプローラの下に [プロパティ] ウィンドウが表示され、役に立つ多くの情報が示されます。最初に [Asset Name] を見てみましょう。これは Content.Load に渡した名前です。ファイル名とは異なる名前を使用したい場合は、この値を変更します。

次に、[Content Processor] の左側にある小さい [+] をクリックして展開します。ここで注意するプロパティは [Color Key Color] です。色がカラー キーと一致するすべてのピクセルは透明になります。これを使用して、四角形のイメージから背景を除去します。つまり、スプライトの背景を取り除くには、背景の色またはカラー キーの設定を、両方が一致するように変更します。Red、Green、Blue、Alpha の各値の既定値は 255、0、255、255 で、これはマゼンタです。

動かしてみよう

確かに、青い背景に描画された 1 つのスプライトがゲームをするうえでそれほど重要になるとは思えません。少なくとも、ユーザーがスプライトを移動できるようにする必要があります。そのためには、ユーザーが何をしたいのかを知る必要があります。プログラムの Update 関数を見た読者は既にその方法に気付いていることでしょう。既定のコードは、入力を読み取り、戻りキーが押されると終了します。惜しいのは、このとき入力が占有されることです。そこで、自分でも入力を使用できるようにするために、少し変更を加えてみましょう。

現在は次のようになっています。

if (GamePad.GetState(PlayerIndex.One).Buttons.Back == 
    ButtonState.Pressed)
    this.Exit();

これを次のように変更します。

GamePadState input = 
    GamePad.GetState(PlayerIndex.One);
if (input.Buttons.Back == ButtonState.Pressed)
    this.Exit();

これで、input 変数を使用してユーザーの入力を見ることができるようになりました。入力が GamePadState であることに注意してください。Zune の入力は、一部のボタンがない Xbox ゲームパッドとします。図 2 は、Zune のボタンに対するマッピングの詳細な一覧です。

図 2 Zune ボタンのマッピング
ボタン マッピング
戻る GamePadState.Buttons.Back
再生/一時停止 GamePadState.Buttons.B
DPad の中央 GamePadState.Buttons.A
DPad の端 GamePadState.DPad
ZunePad 上のスライド GamePadState.Thumbsticks.Left
ZunePad のクリック GamePadState.Buttons.LeftShoulder

それでは player を動かしてみましょう。図 3 のコードは DPad と ZunePad の両方の入力に対応しているので、すべての Zune で動作します (もともとの Zune には ZunePad 機能はありませんでした)。

図 3 移動のサポート

// add these lines to Update()
const int c_speedScalar = 100;
Vector2 speed = Vector2.Zero;

speed.X = input.ThumbSticks.Left.X * c_speedScalar;
speed.Y = input.ThumbSticks.Left.Y * c_speedScalar * -1;

if (input.DPad.Left == ButtonState.Pressed)
    speed.X -= c_speedScalar;
else if (input.DPad.Right == ButtonState.Pressed)
    speed.Y += c_speedScalar;

if (input.DPad.Up == ButtonState.Pressed)
    speed.Y -= c_speedScalar;
else if (input.DPad.Down == ButtonState.Pressed)
    speed.Y += c_speedScalar;

position += speed * (float)gameTime.ElapsedGameTime.TotalSeconds;

Draw ルーチンは position メンバ変数で指定された位置に player を描画していることを思い出してください。したがって、スプライトを移動するには、その位置を更新するだけで十分です。

このコードで本当に重要な部分は、位置を変更している最後の行です。Xbox は Zune より強力なグラフィックス機能を備えているので、Update ルーチンがいっそう頻繁に呼び出されます。つまり、フレーム レートが高いということです。これは、フレーム レートとは関係なく移動を行う必要があることを意味します。そのためには、入力を使用して位置ではなく速度を計算します。次に、前回の Update の呼び出しから経過した時間を乗算することで、速度を位置に変換します。

ボックスの中のスプライト

移動はだれにでもできます。腕の見せ所は、いつ停止するのかをどうやって知るかということです。

ゲームをおもしろくするなら、あるスプライトと別のスプライトが衝突したことを知る必要があります。ゲーム開発者はこれを衝突検出と呼び、XNA Game Studio で衝突検出を行う最も簡単な方法は、BoundingBox という名前のオブジェクトを使用することです。基本的には、オブジェクトを見えないボックスで囲み、ボックスどうしが交差しているかどうかを検査します。

ゲームがたいしておもしろくないことを除けば、最大の問題はスプライトが画面の外に移動する可能性があることです。これを修正します。スプライトが画面の端に達したら、常に (100, 100) の位置に戻すようにします。

まず、画面に対する境界ボックスが必要です。

// Add this member variable
BoundingBox bbScreen;

// Add these lines to Initialize() 
Vector3 min = new Vector3(0, 0, 0);
Vector3 max = new Vector3(graphics.GraphicsDevice.Viewport.Width,
    graphics.GraphicsDevice.Viewport.Height, 0);
bbScreen = new BoundingBox(min, max);

ここではちょっとした技を使用していますが、それほど複雑なものではありません。Hello World フレームワークには graphics という名前のメンバ変数があり、ゲーム画面の幅と高さが含まれています。境界ボックスの左上隅を画面の左上隅に設定し、境界ボックスの右下隅を画面の右下隅に設定します。BoundingBoxes には 3D ベクトルが必要なので、z 座標は 0 に設定します。

そして、スプライトのボックスを次のように作成します。

// Add these lines to Update() after 
// the position has been calculated.
Vector3 min = 
    new Vector3((int)position.X, (int)position.Y, 0);
Vector3 max = 
    new Vector3((int)position.X + texture.Width - 1,
    (int)position.Y + texture.Height - 1, 0);
BoundingBox bb = new BoundingBox(min, max);

最後に、スプライトが画面に完全に収まっているかどうかを調べます。収まっていない場合は、端に達しているはずです。前に追加した行のすぐ下に、次の行を追加します。

ContainmentType ct = bbScreen.Contains(bb);
if (ct != ContainmentType.Contains)
    position = new Vector2(100, 100);

これで、スプライトが画面の端に衝突すると、スプライトは必ず (100, 100) にジャンプして戻ります。もちろん、停止したり、反対側に回り込んだりというような、より高度な処理が必要になることもあります。

画面の端との衝突は、他のオブジェクトとの衝突とは若干異なります。普通の状況では画面に対しては内部に存在しますが、他のオブジェクトに対しては外部に存在します。したがって、画面の端との衝突を調べるには、画面のボックスに含まれているかどうかを確認する必要があります。一方、別のオブジェクトとの衝突を調べる場合は、そのオブジェクトのボックスと交差しているかどうかを確認する必要があります。そのために、BoundingBox オブジェクトには Intersects メンバ関数が用意されています。Intersects は、2 つのボックスが交差していると true になるブール値を返します。2 つのスプライトが相互に衝突しているかどうかを調べる場合は、Intersects を使用します。

音を鳴らす

独自のゲームを完成させるために必要な技術はあと 1 つです。イメージを描画し、ユーザーの入力に対応してイメージを移動し、イメージどうしが衝突したときに対処できるようになりました。これはゲームなので、スプライトが衝突したら派手に爆発させてみるのもいいのではないでしょうか。爆発を描画する方法はもうわかっていますが、プレーヤーのスピーカーも鳴動させることができれば格段に満足度が上がります。さいわいなことに、サウンドの再生はスプライトを描画するよりも簡単です。

まず、サウンドをプロジェクトに追加する必要があります。この処理はイメージを追加するときと同様です。ソリューション エクスプローラに移動し、[Content] を右クリックし、[追加] をポイントして [既存の項目] をクリックします。ここでは、.wav、.wma、.mp3、または .xap ファイルを選択できます。また、イメージ ファイルと同じように、ファイル名とは異なるアセット名を使用する場合は、プロパティで変更できます。

それではサウンドを再生してみましょう。次のコードでは、explode.wav というファイルを読み込んでいます。

// Add this member variable
SoundEffect sndBoom;

// Add these lines to LoadContent()
ContentManager contentManager = 
    new ContentManager(Services, "Content");
sndBoom = 
    contentManager.Load<SoundEffect>("explode");

// Add these lines to Update()
if (input.Buttons.B == ButtonState.Pressed)
    sndBoom.Play();

contentManager.Load の呼び出しは、スプライトのテクスチャの読み込みに使用した関数と似ています。つまり、ご覧のように、サウンドを再生したいときは Play を呼び出すだけです。このコードは、B ボタンが押されている間、すべてのフレームで新しいサウンドを再生することに注意してください。サウンドの長さによっては、再生されるサウンドの数が多くなりすぎて、ゲームが例外をスローする可能性があります。ボタンが押されるたびに 1 回だけ再生するようにして、例外を防ぐ必要があります。これを行う従来の方法として、以前の入力の状態を保存しておき、状態が変化したときにだけ対応するという方法があります。

高度な描画

ここまでで、ゲームを自作するために必要な基本的なツールが揃いました。しばらくこれらのツールで遊んでみて、ツールに慣れることをお勧めします。基本ツールに慣れたら、このセクションで説明する高度なアイデアの概要を読んでください。このセクションで説明する機能については、機能を使用するために知っておく必要のあるすべての情報がヘルプ ドキュメントに提供されています。

最初に、Z オーダーについて説明します。現在のコードもそうなっていますが、2 つのスプライトを重ねると、後から描画したスプライトが上になります。これでは困る場合があります。もっとよい方法は、描画順序を示す変数を各スプライトに設定することです。そのためには、次に示すように、さらに複雑な spriteBatch.Begin と spriteBatch.Draw を使用する必要があります。

spriteBatch.Begin(
    SpriteBlendMode.AlphaBlend, 
    SpriteSortMode.BackToFront, 
    SaveStateMode.None);
spriteBatch.Draw(texture, position, 
    null, Color.White, 0, Vector2.Zero, 
    1.0F, SpriteEffects.None, zOrder);

しばらく SpriteBatch のドキュメントで調べてみてください。これらの関数はいくぶん複雑ですが、非常に強力でもあります。たとえば、スプライトのサイズを変更する場合は、位置のベクトルの代わりに四角形を使用できます。四角形の境界がテクスチャのサイズと異なる場合は、XNA Game Studio が自動的に拡大縮小します。テクスチャに複数のイメージがあり、それを順番に使用したい場合は、テクスチャの描画する部分を指定するソース四角形を渡すことができます。

スプライトを半透明にする場合は、描画に渡す色のアルファ値を変更します。

Color trans = new Color(
    Color.White.R, Color.White.G, 
    Color.White.B, 128);

Color.White の代わりに trans を Draw に渡すと、スプライトは半透明になります。

最後に、次に示す洗練されたコードを使用すると、画面を縦長から横長に回転できます。

// Add this member variable
Matrix matTransform;

// Add these lines to the constructor
matTransform = Matrix.Identity;
matTransform *= 
    Matrix.CreateRotationZ(MathHelper.ToRadians(90));
matTransform *= 
    Matrix.CreateTranslation(240, 0, 0);

// In Draw() change spriteBatch.Begin to this
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, 
    SpriteSortMode.BackToFront, SaveStateMode.None, 
    matTransform);

後はあなたしだい

この記事で使用した例の完全なコードを図 4 に示します。さらに理解が深まるでしょう。困ったときには、creators.xna.com の Web サイトにある豊富なサンプルを参照してください。ピクセル単位の境界ボックスから物理モデルまで、難解なゲーム開発で必要なあらゆる種類のことを行う方法がわかります。

図 4 簡単な Zune ゲーム

public class Game1 : Microsoft.Xna.Framework.Game {
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    Matrix matTransform;
    Texture2D texture;
    SoundEffect sndBoom;
    BoundingBox bbScreen;
    Vector2 position;

    public Game1() {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        position = new Vector2(100, 100);
        matTransform = Matrix.Identity;
        // Uncomment these lines to do Landscape Mode
        //matTransform *= Matrix.CreateRotationZ(          MathHelper. ToRadians(90));
        //matTransform *= Matrix.CreateTranslation(240, 0, 0);
    }

    protected override void Initialize() {
        Vector3 min = new Vector3(0, 0, 0);
        Vector3 max = 
            new Vector3(
            graphics.GraphicsDevice.Viewport.Width, 
            graphics.GraphicsDevice.Viewport.Height, 0);
        bbScreen = new BoundingBox(min, max);
        base.Initialize();
    }

    protected override void LoadContent() {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        texture = Content.Load<Texture2D>("player");
        ContentManager contentManager = 
            new ContentManager(Services, @"Content\Audio");
        sndBoom = contentManager.Load<SoundEffect>("explode");
    }

    protected override void UnloadContent() {
    }

    protected override void Update(GameTime gameTime) {
        GamePadState input = GamePad.GetState(PlayerIndex.One);
        if (input.Buttons.Back == ButtonState.Pressed)
            this.Exit();

        if (input.Buttons.B == ButtonState.Pressed)
            sndBoom.Play();

        const int c_speedScalar = 100;
        Vector2 speed = Vector2.Zero;

        speed.X = input.ThumbSticks.Left.X * c_speedScalar;
        speed.Y = input.ThumbSticks.Left.Y * c_speedScalar * -1;

        if (input.DPad.Left == ButtonState.Pressed)
            speed.X -= c_speedScalar;
        else if (input.DPad.Right == ButtonState.Pressed)
            speed.X += c_speedScalar;

        if (input.DPad.Up == ButtonState.Pressed)
            speed.Y -= c_speedScalar;
        else if (input.DPad.Down == ButtonState.Pressed)
            speed.Y += c_speedScalar;

        position += speed * (float)gameTime.ElapsedGameTime.TotalSeconds;

        Vector3 min = new Vector3((int)position.X, (int)position.Y, 0);
        Vector3 max = new Vector3((int)position.X + 
            texture.Width - 1, (int)position.Y + texture.Height - 1, 0);
        BoundingBox bb = new BoundingBox(min, max);

        ContainmentType ct = bbScreen.Contains(bb);
        if (ct != ContainmentType.Contains)
            position = new Vector2(100, 100);

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime) {
        GraphicsDevice.Clear(Color.CornflowerBlue);
        spriteBatch.Begin(SpriteBlendMode.AlphaBlend, 
            SpriteSortMode.BackToFront, SaveStateMode.None, matTransform);
        spriteBatch.Draw(texture, position, null, Color.White, 
            0, Vector2.Zero, 1.0F, SpriteEffects.None, 0.5F);
        spriteBatch.End();

        base.Draw(gameTime);
    }
}

読者も私と同じくらい C# での XNA ゲーム開発を楽しまれることを期待します。作成したゲームが業界の話題をさらうことになるかもしれません。もっとも、私の息子に負けなければですが。コーディングを楽しんでください。

ご意見やご質問は goplaces@microsoft.com まで英語でお送りください。

Mike Calligaro は、マイクロソフトの Windows Mobile チームのシニア開発担当者、および Windows Mobile チームのブログ (blogs.msdn.com/windowsmobile) の投稿者です。